package ca.mcgill.model.functional.generator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import ca.mcgill.model.functional.Action;
import ca.mcgill.model.functional.AsynchronousFSMsNetwork;
import ca.mcgill.model.functional.FiniteStateMachine;
import ca.mcgill.model.functional.State;
import ca.mcgill.model.functional.Transition;
import ca.mcgill.model.functional.triggering.ExternalEvent;
import ca.mcgill.model.functional.triggering.InternalEvent;
import ca.mcgill.model.implementation.ImplementationModel;
import ca.mcgill.model.implementation.Task;
import ca.mcgill.model.implementation.Transaction;

public class TestCasesGenerator {

	/**
	 * This methods is generating a random use case with properties that can be
	 * defined through the method parameters.
	 * 
	 * @param maxUtilization
	 *            maximal utilization - use case generation algorithm will try
	 *            to assign the WCETs to the actions so the utilization will be
	 *            close to the one specified with the parameter, but not greater
	 * @param maxNbOfFsmsPerTransaction
	 *            determines the range for the number of FSMs per one one
	 *            transaction - 1 to maxNbOfFsmsPerTransaction
	 * @param maxNbOfStatesPerFsm
	 *            determines the range for the number of states per FSM, i.e. 1
	 *            to maxNbOfStatesPerFsm
	 * @param maxIncTransitions
	 *            determines the range for the number of incoming transitions
	 *            per state, i.e. 0 to maxIncTransitions
	 * @param nbOfTransactions
	 *            number of transactions that will be created
	 * @param maxNbOfInternalEvents
	 *            determines the range of the number of internal events between
	 *            each two communicating FSM blocks. The range is from 1 to
	 *            maxNbOfInternalEvents.
	 * @return an instance of an implementation model
	 */
	public ImplementationModel generateTestCase(double maxUtilization,
			int maxNbOfFsmsPerTransaction, int maxNbOfStatesPerFsm,
			int maxIncTransitions, int nbOfTransactions,
			int maxNbOfInternalEvents) {

		ImplementationModel implModel;

		List<FiniteStateMachine> fsms = new ArrayList<FiniteStateMachine>();

		Random rand = new Random();

		int statesNumber;

		List<List<FiniteStateMachine>> fsmsOfTransactions = new ArrayList<List<FiniteStateMachine>>();

		for (int transactionN = 1; transactionN <= nbOfTransactions; transactionN++) {

			FiniteStateMachine fsm = null;

			List<FiniteStateMachine> transactionFsms = new ArrayList<FiniteStateMachine>();

			int numberOfFsms = rand.nextInt(maxNbOfFsmsPerTransaction) + 1;

			int period = generateRandomPeriod();

			ExternalEvent extE = null;

			List<InternalEvent> intEvents = null;
			Set<InternalEvent> usedIntEvents = null;

			for (int i = 1; i <= numberOfFsms; i++) {

				statesNumber = rand.nextInt(maxNbOfStatesPerFsm) + 1;
				fsm = generateFSM(statesNumber, maxIncTransitions, i);

				transactionFsms.add(fsm);

				fsms.add(fsm);

				/*
				 * In this case I am assigning the
				 */
				if (i == 1) {

					extE = new ExternalEvent("extE_" + transactionN, period);

					for (Transition transition : fsm.getTransitions()) {

						transition.setTrigE(extE);

						extE.addTriggeredTransition(transition);

					}
				} else {

					intEvents = new ArrayList<InternalEvent>(usedIntEvents);

					for (Transition transition : fsm.getTransitions()) {

						int eventNb = rand.nextInt(intEvents.size());

						transition.setTrigE(intEvents.get(eventNb));

						intEvents.get(eventNb).addTriggeredTransition(
								transition);

					}

				}

				intEvents = new ArrayList<InternalEvent>();

				int nbOfEvents = rand.nextInt(fsm.getTransitions().size()) + 1;

				/*
				 * Creating internal events.
				 */
				for (int e = 1; e <= nbOfEvents; e++) {

					InternalEvent intEv = new InternalEvent("ev_"
							+ transactionN + "_" + e);

					intEvents.add(intEv);

				}

				usedIntEvents = new HashSet<InternalEvent>();

				/*
				 * Setting the transitions which produce internal events.
				 */
				for (Transition transition : fsm.getTransitions()) {

					InternalEvent outEv = intEvents.get(rand.nextInt(intEvents
							.size()));

					/*
					 * Adding those events which will be ultimately used as an
					 * output events.
					 */
					usedIntEvents.add(outEv);

					transition.setOutE(outEv);

				}

			}

			fsmsOfTransactions.add(transactionFsms);
		}

		AsynchronousFSMsNetwork fsmNetwork = new AsynchronousFSMsNetwork(fsms);

		List<Transaction> transactions = generateTransactions(fsmNetwork,
				fsmsOfTransactions);

		List<Task> tasks = new ArrayList<Task>();

		for (Transaction trans : transactions) {
			for (Task task : trans.getTasksInTransaction()) {
				tasks.add(task);
			}
		}

		implModel = new ImplementationModel(fsmNetwork, tasks, transactions);

		/*
		 * Calling the method that will set the execution times of actions.
		 */
		setExecutionTimes(maxUtilization, implModel, true);

		return implModel;

	}

	/**
	 * This method generates the FSM block with properties as defined in the
	 * method parameters.
	 * 
	 * @param nbOfStates
	 *            number of states to generate
	 * @param maxIncTransitions
	 *            defines the range for the number of incoming transitions per
	 *            each state - 1 to maxIncTransitions
	 * @param fsmNb
	 *            number of the generated FSM - it is used only to construct the
	 *            name of the FSM, its states and actions.
	 * @return an instance of a finite state machine
	 */
	private FiniteStateMachine generateFSM(int nbOfStates,
			int maxIncTransitions, int fsmNb) {

		FiniteStateMachine resultFSM;

		List<State> states = new ArrayList<State>();

		List<Transition> createdTransitions = new ArrayList<Transition>();

		Random rand = new Random();

		int numberOfTrns;

		int trnsNumber = 1;

		/*
		 * Generating a set of states.
		 */
		for (int i = 0; i < nbOfStates; i++) {

			State state = new State("FSM" + fsmNb + "::s" + i + 1);

			states.add(state);

			numberOfTrns = rand.nextInt(maxIncTransitions) + 1;

			/*
			 * Generating a set of transitions for the created state.
			 */
			for (int j = 0; j < numberOfTrns; j++) {

				/*
				 * Normally here when creating an Activity I would specify the
				 * WCET and BCET. However this is done later when calling the
				 * method setExecutionTimes because now the WCETs needs to be
				 * set in a way that the demanded utilization will be respected.
				 * In order to do this I need to first create all activities.
				 */
				Action action = new Action("FSM" + fsmNb + "::act" + trnsNumber);

				Transition trans = new Transition("FSM" + fsmNb + "::trns"
						+ trnsNumber, action);

				trans.setDstState(state);

				createdTransitions.add(trans);

				trnsNumber++;

			}

		}

		List<Transition> unattachedTrns = new ArrayList<Transition>(
				createdTransitions);

		Map<State, Set<State>> depMap = new HashMap<State, Set<State>>();

		for (int i = 0; i < states.size(); i++) {

			State state = states.get(i);

			Transition transToAttach = null;
			int selectedTr;

			do {

				selectedTr = rand.nextInt(unattachedTrns.size());
				transToAttach = unattachedTrns.get(selectedTr);

			} while ((states.size() > 1 && transToAttach.getDstState() == state)
					|| (depMap.get(transToAttach.getDstState()) != null
							&& depMap.get(transToAttach.getDstState())
									.contains(state) && i < states.size() - 1));

			transToAttach.setSrcState(state);
			unattachedTrns.remove(transToAttach);

			Set<State> depStates = null;
			if ((depStates = depMap.get(state)) == null) {
				depStates = new HashSet<State>();
				depMap.put(state, depStates);
			}

			if (depMap.get(transToAttach.getDstState()) != null) {
				depStates.addAll(depMap.get(transToAttach.getDstState()));
			}

			depStates.add(transToAttach.getDstState());

			// Updating the map correspondingly to the choice.
			for (State st : depMap.keySet()) {

				if (depMap.get(st).contains(state)) {

					depMap.get(st).addAll(depMap.get(state));

				}

			}

		}

		for (Transition trns : unattachedTrns) {

			State stateToAttach = null;

			int selectedEl = rand.nextInt(states.size());

			stateToAttach = states.get(selectedEl);

			trns.setSrcState(stateToAttach);

		}

		resultFSM = new FiniteStateMachine("FSM" + fsmNb, createdTransitions,
				states);

		return resultFSM;

	}

	/**
	 * This method is used to generate the transactions, specifically the tasks
	 * and their priorities. Priorities are assigned randomly however they
	 * respect the ascending priorities order for tasks within the same
	 * transaction.
	 * 
	 * @param network
	 *            network of finite state machines
	 * @param fsmsOfTransactions
	 *            list of lists. Each list correspond to one transaction and
	 *            holds fsms that will be within this transaction.
	 * @return list of transactions
	 */
	private List<Transaction> generateTransactions(
			AsynchronousFSMsNetwork network,
			List<List<FiniteStateMachine>> fsmsOfTransactions) {

		List<Transaction> result = new ArrayList<Transaction>();

		List<Integer> priorities = new ArrayList<Integer>();

		Random rand = new Random();

		int maxPriority = network.getFsms().size();

		for (int i = 1; i <= maxPriority; i++) {
			priorities.add(i);
		}

		int priority, min, max;

		for (List<FiniteStateMachine> fsms : fsmsOfTransactions) {

			List<Task> tasksOfTransaction = new ArrayList<Task>();

			max = priorities.size() - fsms.size();
			min = 0;

			for (int i = 1; i <= fsms.size(); i++) {

				int chosenIndex;

				if (max < min) {
					chosenIndex = 0;
				} else {
					chosenIndex = rand.nextInt(max - min + 1) + min;
				}

				priority = priorities.remove(chosenIndex);

				// max = max - 1;

				min = chosenIndex;

				Task task = new Task("Task(" + fsms.get(i - 1).getName() + ")",
						fsms.get(i - 1), priority);

				tasksOfTransaction.add(task);

			}

			Transaction transaction = new Transaction(tasksOfTransaction,
					((ExternalEvent) fsms.get(0).getTransitions().get(0)
							.getTrigE()).getPeriod());

			result.add(transaction);

		}

		return result;

	}

	private int generateRandomPeriod() {

		Random rand = new Random();

		return (rand.nextInt(10) + 1) * 10;

	}

	/**
	 * This method sets the WCETs and BCETs of actions so in total the
	 * utilization will be close to the specified 'utilization'.
	 * 
	 * @param utilization
	 * @param implModel
	 * @param useIntValues
	 *            tells whether the assigned values for the WCETs and BCETs
	 *            should be integer values. Otherwise we assign double values.
	 */
	private void setExecutionTimes(double utilization,
			ImplementationModel implModel, boolean useIntValues) {

		double multiplication = 1;

		for (Transaction transaction : implModel.getTransactions()) {

			multiplication = multiplication
					* ((ExternalEvent) transaction.getTasksInTransaction()
							.get(0).getFsm().getStates().get(0)
							.getInTransition().get(0).getTrigE()).getPeriod();

		}

		double divisor = 0;

		int period;

		for (Transaction transaction : implModel.getTransactions()) {

			period = ((ExternalEvent) transaction.getTasksInTransaction()
					.get(0).getFsm().getStates().get(0).getInTransition()
					.get(0).getTrigE()).getPeriod();

			divisor += (multiplication / period)
					* transaction.getTasksInTransaction().size();

		}

		double averageWcet = utilization * multiplication / divisor;

		Random rand = new Random();

		for (Transaction transaction : implModel.getTransactions()) {

			period = ((ExternalEvent) transaction.getTasksInTransaction()
					.get(0).getFsm().getStates().get(0).getInTransition()
					.get(0).getTrigE()).getPeriod();

			for (Task task : transaction.getTasksInTransaction()) {

				for (Transition transition : task.getFsm().getTransitions()) {

					if (!useIntValues) {
						double wcet = averageWcet;

						if (averageWcet > period) {
							wcet = period - 1;
						}

						if (rand.nextInt(2) == 0) {
							wcet -= rand.nextDouble();
						} else {
							wcet += rand.nextDouble();
						}

						transition.getAction().setWCET(wcet);

						double bcet = wcet - rand.nextInt(3) * (wcet / 3);

						transition.getAction().setBCET(bcet);
					} else {
						int wcet = (int) averageWcet;

						if (averageWcet > period) {
							wcet = period - 1;
						}

						if (rand.nextInt(2) == 0) {
							wcet -= rand.nextInt(2);
						} else {
							wcet += rand.nextInt(2);
						}

						transition.getAction().setWCET(wcet);

						int bcet = wcet - rand.nextInt(3) * (wcet / 3);

						transition.getAction().setBCET(bcet);
					}

				}

			}

		}
	}

	/**
	 * This method can be used mostly for the validation purpose to see what is
	 * the utilization of the FSM network implementation.
	 * 
	 * @param implModel
	 *            input model for which the utilization will be checked
	 * @return utilization
	 */
	public double checkTheAverageUtilization(ImplementationModel implModel) {

		double utilization = 0;

		for (Transaction transaction : implModel.getTransactions()) {

			double period = ((ExternalEvent) transaction
					.getTasksInTransaction().get(0).getFsm().getStates().get(0)
					.getInTransition().get(0).getTrigE()).getPeriod();

			for (Task task : transaction.getTasksInTransaction()) {

				double averageWcet = 0;

				for (Transition transition : task.getFsm().getTransitions()) {

					averageWcet += transition.getAction().getWCET();

				}

				averageWcet = averageWcet
						/ task.getFsm().getTransitions().size();

				utilization += averageWcet / period;
			}

		}

		return utilization;

	}

}
